import fg from 'fast-glob';
import fs from 'fs-extra';
import path from 'path';
import { ConnectConfig } from "ssh2";
import { toolchain } from '../../toolchain.js';
import { Cmd } from '../../tools/Cmd.js';
import { SSHClient } from '../../tools/SSHClient.js';
import { fileSize, md5 } from '../../tools/vendor.js';
import { BaseTask, TaskResult } from './BastTask.js';

export class PublishResTask extends BaseTask {
    /**下面这些文件类型的文件都是唯一的，忽略对它们的md5的比较*/
    private readonly ingorehashexts = ['.ab', '.apk', '.zip', '.ipa', '.map', '.bytes'];

    async run(): Promise<TaskResult<void>> {
        const cfg = toolchain.params.unityResPublishCfg;
        console.log('res publish cfg:', cfg);

        fs.ensureDirSync(path.dirname(cfg.verJsonPath));
    
        await fs.remove(cfg.srcResfile);
        await fs.remove(cfg.tmpResDir);

        await fs.remove(cfg.srcSrcMapfile);
        await fs.remove(cfg.tmpSrcMapDir);
        
        let [hasdiff, md5Map, apkDiffs] = await this.diffLastVer();
        if(hasdiff) {
            if (fs.existsSync(cfg.tmpResDir))
                await new Cmd().run('tar', ['-C', cfg.tmpResDir, '-czvf', cfg.srcResfile, '.'], { silent: true });
            if (fs.existsSync(cfg.tmpSrcMapDir))
                await new Cmd().run('tar', ['-C', cfg.tmpSrcMapDir, '-czvf', cfg.srcSrcMapfile, '.'], { silent: true });

            const cdn = toolchain.params.cdnHostInfo;
            await this.upLoadRes(cdn.hostinfo, cdn.cdnDir, cfg.srcResfile, cfg.dstResfile, true);
            await this.upLoadRes(cdn.srcMapHostinfo, cdn.srcMapCdnDir, cfg.srcSrcMapfile, cfg.dstSrcMapfile, false);

            fs.writeJsonSync(cfg.verJsonPath, md5Map, { spaces: 2 });
            if(apkDiffs.length > 0) {
                console.log('*** warning apk change list:')
                for(let sf of apkDiffs)
                    console.log(`  * ${sf}`);
                console.log('upload res finished! but apk changed, please confirm!!!')
            }
        }
        console.log('upload res finished!');
        return { success: true, errorCode: 0 };
    }

    /**
     * 除了txt外，其他都以文件名为准，因为我们的文件名都是唯一的
     */
    private async diffLastVer(): Promise<[hasdiff: boolean, curmd5s: {[file: string]: string}, apkDiffs: string[]]> {
        console.log('find all files ...');
        const cfg = toolchain.params.unityResPublishCfg;
        
        console.log('start calc file md5 (only txt files)...');
        const pattern = [], ignorePattern = [];
        if (this.cmdOption.platform == 'Android') {
            pattern.push('assets/android/**/*');
            // pattern.push('apk/**/*');
        } else if (this.cmdOption.platform == 'iOS') {
            pattern.push('assets/ios/**/*');
        } else if (this.cmdOption.platform == 'WebGL') {
            if (this.cmdOption.webglRuntime == 'minigame' && toolchain.unity.supportCompressTex()) {
                pattern.push('assets/wx/**/*');
                // 小游戏版本号已经内置在cs代码里，不再需要version.txt
                ignorePattern.push('assets/wx/**/version.txt');
            } else {
                pattern.push('assets/webgl/**/*', 'browser/**/*');
            }
            // webgl项目不需要.js的ab包
            ignorePattern.push('**/tsbytes/**/*');
        } else if (this.cmdOption.platform == 'Windows') {
            pattern.push('assets/windows/**/*');
            pattern.push('exe/**/*');
        } else {
            console.error('platform not supported:', this.cmdOption.platform);
            process.exit(1);
        }
        const files = await fg(pattern, { cwd: cfg.srcDirPath, ignore: ignorePattern });
        const curmd5s: {[file: string]: string} = {};
        for(let file of files) {
            let sfile = path.join(cfg.srcDirPath, file);
            if (this.ingorehashexts.includes(path.extname(file))) {
                curmd5s[file] = '';
            } else {
                curmd5s[file] = md5(await fs.readFile(sfile));
            }
        }
        console.log('end calc file md5!')
    
        console.log('load last version files ...')
        // last build md5
        let lastmd5s: {[file: string]: string} = {};
        if(fs.existsSync(cfg.verJsonPath)) {
            lastmd5s = fs.readJsonSync(cfg.verJsonPath, {encoding: 'utf-8'}) as {[file: string]: string};
            // python版的资源外发写入的路径为windows风格，此处通过normalize统一为posix风格
            const winKeys: string[] = [];
            for(const sf in lastmd5s) {
                const posixSf = path.normalize(sf);
                if (sf == posixSf) {
                    winKeys.push(sf);
                }
            }
            for(const sf of winKeys) {
                const posixSf = sf.replace(/\\/g, '/');
                if (!(posixSf in lastmd5s)) {
                    lastmd5s[posixSf] = lastmd5s[sf];
                }
                delete lastmd5s[sf];
            }
        } else {
            console.warn('verJsonPath not exists: ', cfg.verJsonPath);
        }
    
        console.log('collect change files ...');
        // diff, and copy
        let diffFiles: string[] = [];
        for(let sf in curmd5s) {
            let lasthash = lastmd5s[sf];
            if(lasthash == null) {
                // 新增的文件
                diffFiles.push(sf);
            } else {
                // 该文件存在
                let curhash = curmd5s[sf];
                if(curhash != '' && curhash != lasthash) {
                    // 该文件为txt/json...文件，同时被修改了
                    diffFiles.push(sf);
                }
            }
        }
        console.log('collect change files end');
        
        console.log('copy files to tmpdir ...');
        for(let sf of diffFiles) {
            let file = path.join(cfg.srcDirPath, sf) as string;
            let tmpfile = "";
            if (file.includes('tsscriptmaps')) {
                tmpfile = path.join(cfg.tmpSrcMapDir, sf);
            } else {
                tmpfile = path.join(cfg.tmpResDir, sf);
            }
            let tmpdir = path.dirname(tmpfile);
            fs.ensureDirSync(tmpdir);
            fs.copyFileSync(file, tmpfile);
        }
        console.log('copy files to tmpdir end');
    
        console.log(`change files count: ${diffFiles.length}`);
        
        // print change list
        console.log('--- change list:');
        for(let sf of diffFiles)
            console.log('  ' + sf);
        
        // collect apk change, print warning log
        let apkDiffs: string[] = [];
        for(let sf in diffFiles)
            if(sf.startsWith('apk/'))
                apkDiffs.push(sf);
        
        // 合并写入json md5
        Object.assign(lastmd5s, curmd5s);
                
        return [diffFiles.length > 0, lastmd5s, apkDiffs];
    }

    private async upLoadRes(hostinfo: ConnectConfig, cdnDir: string, srcResfile: string, dstResfile: string, tryAutoPushCDN: boolean) {
        if (!fs.existsSync(srcResfile)) {
            return;
        }

        console.log('ready to upload!');
        
        const ssh = new SSHClient();
        await ssh.connect(hostinfo);
        await ssh.exec(`rm -f ${dstResfile}`);
        await ssh.put(srcResfile, dstResfile);

        // 获取远程机器os信息
        const osInfo = await ssh.exec('uname -a');
        let md5cmd = 'md5sum';
        if(osInfo.startsWith('Darwin')) {
            md5cmd = 'md5';
        }

        // 比较文件md5
        const remoteMd5stdout = await ssh.exec(`${md5cmd} ${dstResfile}`);
        const remoteMd5 = remoteMd5stdout.match(/[0-9a-fA-F]{32}/)![0].toLowerCase();

        const size = await fileSize(srcResfile);
        if (size < 2147483648) {
            const localMd5 = md5(fs.readFileSync(srcResfile));
            
            if(remoteMd5 != localMd5) {
                console.error('errer: md5 is not equal!')
                process.exit(1);
            }
            console.log('md5 is equal...')            
        } else {
            console.log('file is bigger than 2GB, skip md5 comparison');
        }
        
        await ssh.exec(`mkdir -p ${cdnDir}`);
        await ssh.exec(`tar -xzf ${dstResfile} -C ${cdnDir}`, true);
        await ssh.exec(`chmod -R o+rx ${cdnDir}`);
        if (tryAutoPushCDN && toolchain.params.channelCfg.autoPushCDN) {
            await ssh.exec(`sh ${toolchain.params.channelCfg.autoPushCDN}`);
        }
        await ssh.end();
    }

    get skip(): boolean {
        return !this.cmdOption.uploadRes;
    }
}
